/**
 *  StaDiUM - State Diagram Unified Modeler
 *  Copyright (C) 2007  Stefan Bucur, Mihai Balan, Claudiu Tanase
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.

 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.

 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

/**
 *  File Information
 *
 *  Last Modified On: $Date: 2007-06-04 22:31:22 +0000 (Mon, 04 Jun 2007) $
 *  Last Modified By: $Author: stefan.bucur $
 */
package ro.pub.cs.stadium.platform;

import java.util.Hashtable;
import java.util.Map;

import javax.swing.event.EventListenerList;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;

import ro.pub.cs.stadium.document.IDocument;
import ro.pub.cs.stadium.document.IDocumentItem;
import ro.pub.cs.stadium.document.IState;
import ro.pub.cs.stadium.document.ITransition;
import ro.pub.cs.stadium.document.Point;
import ro.pub.cs.stadium.document.Size;
import ro.pub.cs.stadium.document.logic.State;
import ro.pub.cs.stadium.document.logic.Transition;

/**
 * A custom control that is responsible with document drawing and editing
 * @author stefan
 *
 */
public class DrawingCanvas extends Canvas implements PaintListener,
	MouseListener, MouseMoveListener, MouseTrackListener,
	KeyListener{
	
	private Display display = null;
	
	private final Map<String, Color> colors = 
		new Hashtable<String, Color>();
	
	private boolean drawGrid = true;
	
	private CanvasMode canvasMode = null;
	
	private IDocument theDocument = null;
	
	private Transform docTransform = null;
	private Transform docInvTransform = null;
	
	private boolean mousePressed = false;
	private int mouseButton = 0;
	private int oldMouseX, oldMouseY;
	
	// Selection specific fields
	private final Map<String, IDocumentItem> selection = 
		new Hashtable<String, IDocumentItem>();
	
	private IDocumentItem hoverItem = null;
	
	private final boolean[] hitCP = new boolean[4];
	
	// New transition specific fields
	
	private IState startState = null;
	private IState endState = null;
	
	// Event listeners list
	
	private final EventListenerList drawingEventListenerList =
		new EventListenerList();
	
	
	// Event triggers
	
	private void fireAddTransition(IState startState, IState endState) {
		DrawingEventListener[] listeners = 
			drawingEventListenerList.getListeners(DrawingEventListener.class);
		
		for (int i=listeners.length - 1; i >= 0; i--) {
			if (listeners[i] != null)
				listeners[i].transitionAdded(startState, endState);
		}
	}
	
	private void fireAddState(Point location) {
		DrawingEventListener[] listeners = 
			drawingEventListenerList.getListeners(DrawingEventListener.class);
		
		for (int i=listeners.length - 1; i >= 0; i--) {
			if (listeners[i] != null)
				listeners[i].stateAdded(location);
		}
	}
	
	private void fireItemDoubleClicked(IDocumentItem item) {
		DrawingEventListener[] listeners = 
			drawingEventListenerList.getListeners(DrawingEventListener.class);
		
		for (int i=listeners.length - 1; i >= 0; i--) {
			if (listeners[i] != null)
				listeners[i].itemDoubleClicked(item);
		}
	}
	
	private void fireItemDelete(IDocumentItem item) {
		DrawingEventListener[] listeners = 
			drawingEventListenerList.getListeners(DrawingEventListener.class);
		
		for (int i=listeners.length - 1; i >= 0; i--) {
			if (listeners[i] != null)
				listeners[i].itemDelete(item);
		}
	}
	
	/**
	 * Initializes the colors used to draw the diagram
	 *
	 */
	private void initColors() {
		// General Colors
		colors.put("back", new Color(display, 255, 255, 255)); // Back color
		colors.put("grid", new Color(display, 100, 100, 100)); // Grid color
		colors.put("document", new Color(display, 0, 0, 0)); // Document color
		
		colors.put("border", display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); // Border color
		
		colors.put("disable", display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); // Disable color
		
		// State Colors
		colors.put("state border", new Color(display, 0, 0, 0));
		colors.put("state back", new Color(display, 255, 255, 255));
		
		// Transition Colors
		colors.put("transition", new Color(display, 0, 0, 0));
		
		// Selection Colors
		colors.put("selection", new Color(display, 0, 0, 255));
		colors.put("trans end", new Color(display, 255, 0, 0));
		colors.put("trans control", new Color(display, 0, 255, 0));
		colors.put("bounding", new Color(display, 255, 0, 0));
	}
	
	private void setDocumentTransform(GC gc) {
		docTransform.setElements(1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
		
		float xunit = theDocument.getXUnit().getXSizeInPixels(1.0f, display);
		float yunit = theDocument.getYUnit().getYSizeInPixels(1.0f, display);
		
		Point viewPosition = theDocument.getViewPosition();
		
		docTransform.scale(xunit, yunit);
		docTransform.translate(viewPosition.coords[0], viewPosition.coords[1]);
		docTransform.scale(theDocument.getZoom(), theDocument.getZoom());
		
		float[] elem = new float[6];
		docTransform.getElements(elem);
		
		docInvTransform.setElements(elem[0], elem[1], elem[2],
				elem[3], elem[4], elem[5]);
		docInvTransform.invert();
	}
	
	private Transform getDocumentTransform() {
		return docTransform;
	}
	
	private Transform getInvDocumentTransform() {
		return docInvTransform;
	}
	
	// GENERAL PURPOSE DRAWING ROUTINES
	
	/**
	 *  @param t a floating point value in range [0,1]
	 */
	private Point getCubicBezierPos(float[] cp, float t) {
		float x, y;
		
		x  = cp[0]*(1-t)*(1-t)*(1-t) +
		   3*cp[2]*t*(1-t)*(1-t) +
		   3*cp[4]*t*t*(1-t) +
		     cp[6]*t*t*t;
		
		y  = cp[1]*(1-t)*(1-t)*(1-t) +
		   3*cp[3]*t*(1-t)*(1-t) +
		   3*cp[5]*t*t*(1-t) +
		     cp[7]*t*t*t;
		
		return new Point(x, y);
	}
	
	/**
	 * @param cp  the vector of control points coordinates
	 * @param steps  the number of drawing iterations
	 */
	private void drawCubicBezier(float[] cp, int steps, GC gc) {
		// TODO: Optimize this
		float t; // the position in the curve
		float tnext; // the next position in the curve
		
		float x1, y1; // the coordinates of the current position
		float x2, y2; // the coordinates of the next position
		
		for (int i=0; i < steps; i++) {
			t = (float)i/steps;
			tnext = (float)(i+1)/steps;
			
			x1 = cp[0]*(1-t)*(1-t)*(1-t) +
			   3*cp[2]*t*(1-t)*(1-t) +
			   3*cp[4]*t*t*(1-t) +
			     cp[6]*t*t*t;
			
			y1 = cp[1]*(1-t)*(1-t)*(1-t) +
			   3*cp[3]*t*(1-t)*(1-t) +
			   3*cp[5]*t*t*(1-t) +
			     cp[7]*t*t*t;
			
			x2 = cp[0]*(1-tnext)*(1-tnext)*(1-tnext) +
			   3*cp[2]*tnext*(1-tnext)*(1-tnext) +
			   3*cp[4]*tnext*tnext*(1-tnext) +
			     cp[6]*tnext*tnext*tnext;
			
			y2 = cp[1]*(1-tnext)*(1-tnext)*(1-tnext) +
			   3*cp[3]*tnext*(1-tnext)*(1-tnext) +
			   3*cp[5]*tnext*tnext*(1-tnext) +
			     cp[7]*tnext*tnext*tnext;
			
			gc.drawLine((int)x1, (int)y1,
					(int)x2, (int)y2);
		}
	}
	
	// HIT TEST ROUTINES
	
	private boolean hitTestPoint(int x, int y, Point point, int threshold) {
		getDocumentTransform().transform(point.coords);
		
		if (Math.abs(x - point.coords[0]) <= threshold &&
				Math.abs(y - point.coords[1]) <= threshold) {
			return true;
		}
		
		return false;
	}
	
	private boolean hitTestCubicBezier(int x, int y, float[] cp) {
		// TODO: implement this
		return false;
	}
	
	private boolean hitTestState(int x, int y, IState state) {
		Point statePos = state.getPosition();
		Size stateSize = state.getContentSize();
		
		float[] stateRect = new float[] {
				statePos.coords[0] - stateSize.width/(float)Math.sqrt(2),
				statePos.coords[1] - stateSize.height/(float)Math.sqrt(2), 
				statePos.coords[0] + stateSize.width/(float)Math.sqrt(2),
				statePos.coords[1] + stateSize.height/(float)Math.sqrt(2)
		};
		
		getDocumentTransform().transform(stateRect);
		
		// The ellipse dimensions
		float a = (stateRect[2] - stateRect[0])/2;
		float b = (stateRect[3] - stateRect[1])/2;
		
		float dx = x - (stateRect[2] + stateRect[0])/2;
		float dy = y - (stateRect[3] + stateRect[1])/2;
		
		// Ellipse hit test
		if ((dx*dx + dy*dy)*(dx*dx + dy*dy) <= (a*a)*(dx*dx) + (b*b)*(dy*dy))
			return true;
		else
			return false;
	}
	
	/**
	 * 
	 * @param x
	 * @param y
	 * @param trans
	 * @param testCP boolean values in this order: start, startCP,
	 * 	endCP, end
	 * @return
	 */
	private boolean hitTestTransition(int x, int y, ITransition trans,
			boolean[] testCP) {
		IState start = trans.getStartPoint();
		IState end = trans.getEndPoint();
		
		Point p0 = start.getPosition();
		Point p1 = trans.getStartControlPoint();
		Point p2 = trans.getEndControlPoint();
		Point p3 = end.getPosition();
		
		// The Bezier control points
		float[] controlPoints = new float[] {
			p0.coords[0],
			p0.coords[1],
			p0.coords[0] + p1.coords[0],
			p0.coords[1] + p1.coords[1],
			p3.coords[0] + p2.coords[0],
			p3.coords[1] + p2.coords[1],
			p3.coords[0],
			p3.coords[1]
		};
		
		if (testCP != null) {
			testCP[0] = hitTestPoint(x, y, 
					new Point(controlPoints[0], controlPoints[1]), 5);
			
			testCP[1] = hitTestPoint(x, y,
					new Point(controlPoints[2], controlPoints[3]), 5);
			
			testCP[2] = hitTestPoint(x, y,
					new Point(controlPoints[4], controlPoints[5]), 5);
			
			testCP[3] = hitTestPoint(x, y,
					new Point(controlPoints[6], controlPoints[7]), 5);
		}
		
		Point labelPos = getCubicBezierPos(controlPoints, 0.7f);
		
		getDocumentTransform().transform(controlPoints);
		
		Size labelSize = trans.getLabelSize();
		float[] labelRect = new float[] {
			labelPos.coords[0] - labelSize.width/2,
			labelPos.coords[1] - labelSize.height/2,
			labelPos.coords[0] + labelSize.width/2,
			labelPos.coords[1] + labelSize.height/2
		};
		
		getDocumentTransform().transform(labelRect);
		
		if (x >= labelRect[0] && x <= labelRect[2] &&
				y >= labelRect[1] && y <= labelRect[3])
			return true;
		
		return hitTestCubicBezier(x, y, controlPoints);
		
	}
	
	// PAINTING ROUTINES
	
	/**
	 * Draws the cross in the origin (0,0) of the document.
	 */
	private void drawOrigin(GC gc) {
		Point origin = new Point(0.0f, 0.0f);
		
		getDocumentTransform().transform(origin.coords);
		
		gc.setForeground(colors.get("grid"));
		
		gc.drawLine((int)origin.coords[0] - 5, (int)origin.coords[1],
				(int)origin.coords[0] + 5, (int)origin.coords[1]);
		gc.drawLine((int)origin.coords[0], (int)origin.coords[1] - 5 ,
				(int)origin.coords[0], (int)origin.coords[1] + 5);
	}
	
	/**
	 * Draws the document outline.
	 * @param gc
	 */
	private void drawDocumentBorder(GC gc) {
		Size docSize = theDocument.getDocumentSize();
		float[] border = new float[] {0.0f, 0.0f, docSize.width,
				docSize.height};
		
		getDocumentTransform().transform(border);
		
		gc.setForeground(colors.get("document"));
		gc.setBackground(colors.get("document"));
		
		gc.drawRectangle((int)border[0], (int)border[1],
				(int)(border[2] - border[0]),
				(int)(border[3] - border[1]));
		
		gc.fillRectangle((int)border[0] - 5, (int)border[1] + 5,
				5, (int)(border[3] - border[1]));
		gc.fillRectangle((int)border[0] - 5, (int)border[3],
				(int)(border[2] - border[0]), 5);
	}
	
	/**
	 * Draws the document grid
	 * @param gc
	 */
	private void drawGrid(GC gc) {
	
		Rectangle clientArea = this.getClientArea();
		
		float[] limits = new float[]
		       {clientArea.x, clientArea.y,
				clientArea.width, clientArea.height};
		
		getInvDocumentTransform().transform(limits);
		
		gc.setForeground(colors.get("grid"));
		
		for (int i = (int)Math.ceil(limits[0]); i <= Math.floor(limits[2]); i++) {
			for (int j = (int)Math.ceil(limits[1]); j <= Math.floor(limits[3]); j++) {
				Point crPoint = new Point(i, j);
				getDocumentTransform().transform(crPoint.coords);
				gc.drawPoint((int)crPoint.coords[0], (int)crPoint.coords[1]);
			}
		}
	}
	
	/**
	 * Draws a diagram state
	 * @param state the state to draw
	 * @param gc
	 */
	private void drawState(IState state, GC gc) {
		Point statePos = state.getPosition();
		Size stateSize = state.getContentSize();
		
		float[] stateRect = new float[] {
				statePos.coords[0] - stateSize.width/(float)Math.sqrt(2),
				statePos.coords[1] - stateSize.height/(float)Math.sqrt(2), 
				statePos.coords[0] + stateSize.width/(float)Math.sqrt(2),
				statePos.coords[1] + stateSize.height/(float)Math.sqrt(2)
		};
		
		getDocumentTransform().transform(stateRect);
		
		if (hoverItem == state || selection.containsKey(state.getName())) {
			if (hoverItem == state)
				gc.setForeground(colors.get("state border"));
			else
				gc.setForeground(colors.get("selection"));
			gc.setBackground(colors.get("state back"));
			
			gc.setLineWidth(2);
			
			gc.drawOval(
					(int)stateRect[0],
					(int)stateRect[1],
					(int)(stateRect[2] - stateRect[0]),
					(int)(stateRect[3] - stateRect[1])
				);
			
			gc.setLineWidth(1);
			gc.setForeground(colors.get("bounding"));
			
			gc.drawRectangle(
					(int)stateRect[0]- 3,
					(int)stateRect[1]- 3,
					(int)(stateRect[2] - stateRect[0]) + 6,
					(int)(stateRect[3] - stateRect[1]) + 6
				);
			
		} else {
			gc.setForeground(colors.get("state border"));
			gc.setBackground(colors.get("state back"));
			
			gc.setLineWidth(1);
			
			gc.drawOval(
					(int)stateRect[0],
					(int)stateRect[1],
					(int)(stateRect[2] - stateRect[0]),
					(int)(stateRect[3] - stateRect[1])
				);
		}
		
		String text = "Name: " + state.getName() + "\nActions: " + 
			((State)state).getAllActions();
		
		org.eclipse.swt.graphics.Point textExt = gc.textExtent(text);
		
		gc.drawText(text ,
				(int)(stateRect[0] + stateRect[2])/2 - textExt.x/2, 
				(int)(stateRect[1] + stateRect[3])/2 - textExt.y/2,
				SWT.DRAW_DELIMITER);
		
		
	}
	
	private void drawTransition(ITransition transition, GC gc) {
		IState start = transition.getStartPoint();
		IState end = transition.getEndPoint();
		
		Point p0 = start.getPosition();
		Point p1 = transition.getStartControlPoint();
		Point p2 = transition.getEndControlPoint();
		Point p3 = end.getPosition();
		
		// The Bezier control points
		float[] controlPoints = new float[] {
			p0.coords[0],
			p0.coords[1],
			p0.coords[0] + p1.coords[0],
			p0.coords[1] + p1.coords[1],
			p3.coords[0] + p2.coords[0],
			p3.coords[1] + p2.coords[1],
			p3.coords[0],
			p3.coords[1]
		};
		
		Point labelPos = getCubicBezierPos(controlPoints, 0.7f);
		
		getDocumentTransform().transform(controlPoints);
		
		Size labelSize = transition.getLabelSize();
		float[] labelRect = new float[] {
			labelPos.coords[0] - labelSize.width/2,
			labelPos.coords[1] - labelSize.height/2,
			labelPos.coords[0] + labelSize.width/2,
			labelPos.coords[1] + labelSize.height/2
		};
		
		getDocumentTransform().transform(labelRect);
		
		if (hoverItem == transition || selection.containsKey(transition.getName())) {
			gc.setLineWidth(2);
			if (hoverItem == transition)
				gc.setForeground(colors.get("transition"));
			else
				gc.setForeground(colors.get("selection"));
			
			drawCubicBezier(controlPoints, 20, gc);
			
			gc.drawRoundRectangle(
					(int)labelRect[0], 
					(int)labelRect[1], 
					(int)(labelRect[2] - labelRect[0]),
					(int)(labelRect[3] - labelRect[1]),
					5, 5 
					);
			
			gc.setLineWidth(1);
			gc.setForeground(colors.get("grid"));
			
			gc.drawLine((int)controlPoints[0], (int)controlPoints[1],
					(int)controlPoints[2], (int)controlPoints[3]);
			
			gc.drawLine((int)controlPoints[4], (int)controlPoints[5],
					(int)controlPoints[6], (int)controlPoints[7]);
			
			gc.setForeground(colors.get("trans control"));
			
			gc.drawRectangle((int)controlPoints[2] - 3, (int)controlPoints[3] - 3, 6, 6);
			gc.drawRectangle((int)controlPoints[4] - 3, (int)controlPoints[5] - 3, 6, 6);
			
			gc.setForeground(colors.get("trans end"));
			
			gc.drawRectangle((int)controlPoints[0] - 3, (int)controlPoints[1] - 3, 6, 6);
			gc.drawRectangle((int)controlPoints[6] - 3, (int)controlPoints[7] - 3, 6, 6);
			
		} else {
			gc.setLineWidth(1);
			gc.setForeground(colors.get("transition"));
			
			drawCubicBezier(controlPoints, 20, gc);
			
			gc.drawRoundRectangle(
					(int)labelRect[0], 
					(int)labelRect[1], 
					(int)(labelRect[2] - labelRect[0]),
					(int)(labelRect[3] - labelRect[1]),
					5, 5 
					);
			

			
		
		}
		
		String text = "Name: " + transition.getName() + 
		"\nCondition: " + ((((Transition)transition).getCondition() == null) ?
				"n/a" : ((Transition)transition).getCondition().toString());
		
		org.eclipse.swt.graphics.Point textExt = gc.textExtent(text);
		
		gc.drawText(text ,
				(int)(labelRect[0] + labelRect[2])/2 - textExt.x/2, 
				(int)(labelRect[1] + labelRect[3])/2 - textExt.y/2,
				SWT.DRAW_DELIMITER);
		
		
	}

	/**
	 * Inherited constructor
	 * @param parent
	 * @param style
	 */
	public DrawingCanvas(Composite parent, int style) {
		super(parent, style | SWT.DOUBLE_BUFFERED);
		
		display = getDisplay();
		
		docTransform = new Transform(display);
		docInvTransform = new Transform(display);
		
		initColors();
		
		setCanvasMode(CanvasMode.SELECTING);
		
		setBackgroundMode(SWT.INHERIT_NONE);
		
		addPaintListener(this);
		addMouseListener(this);
		addMouseMoveListener(this);
		addMouseTrackListener(this);
		addKeyListener(this);
	}
	
	// EVENT HANDLING METHODS
	
	// PaintListener Interface

	/**
	 * Occurs when the control needs to be repainted
	 */
	public void paintControl(PaintEvent e) {
		GC gc = e.gc;
		
		gc.setAntialias(SWT.ON);
		
		Rectangle drArea = this.getClientArea();
		
		if (theDocument == null) {
			
			gc.setBackground(colors.get("disable"));
			gc.fillRectangle(drArea);
			
			
		} else {
			// Drawing the diagram
			
			gc.setAntialias(SWT.OFF);
			gc.setBackground(colors.get("back"));
			gc.fillRectangle(drArea);
			
			setDocumentTransform(gc);
			
			if (drawGrid) {
				drawGrid(gc);
				
			}
			
			gc.setAntialias(SWT.ON);
			
			drawDocumentBorder(gc);
			drawOrigin(gc);
			
			for (IState crState : theDocument.getStates().values()) {
				drawState(crState, gc);
			}
			
			for (ITransition crTrans : theDocument.getTransitions().values()) {
				drawTransition(crTrans, gc);
			}
			
			// Draw the dummies
			
			if (canvasMode == CanvasMode.ADD_TRANSITION) {
				if (startState != null) {
					gc.setForeground(colors.get("grid"));
					gc.setLineStyle(SWT.LINE_DASH);
					gc.setLineWidth(2);
					
					Point start = startState.getPosition();
					getDocumentTransform().transform(start.coords);
					
					gc.drawLine((int)start.coords[0], (int)start.coords[1],
							oldMouseX, oldMouseY);
				}
			}
			
			if (canvasMode == CanvasMode.ADD_STATE) {
				gc.setForeground(colors.get("grid"));
				gc.setLineStyle(SWT.LINE_DASH);
				gc.setLineWidth(2);
				
				gc.drawOval(oldMouseX - 20, oldMouseY - 20, 40, 40);
			}
		}
		
		
		gc.setLineStyle(SWT.LINE_SOLID);
		gc.setLineWidth(1);
		gc.setForeground(colors.get("border"));
		gc.drawRectangle(drArea.x, drArea.y,
				drArea.width - 1, drArea.height - 1);	
		
	}
	
	// MouseListener Interface

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseDoubleClick(MouseEvent e) {
		if (canvasMode == CanvasMode.SELECTING) {
			if (hoverItem != null && isSelected(hoverItem.getName())) {
				fireItemDoubleClicked(hoverItem);
			}
		}
		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseDown(MouseEvent e) {
		mousePressed = true;
		mouseButton = e.button;
		
		if (canvasMode == CanvasMode.ADD_STATE) {
			if (e.button == 1) {
				Point location = new Point(e.x, e.y);
				getInvDocumentTransform().transform(location.coords);
				
				fireAddState(location);
			}
		}
		
		if (canvasMode == CanvasMode.ADD_TRANSITION) {
			if (e.button == 1) {
				if (hoverItem != null && hoverItem instanceof IState) {
					startState = (IState)hoverItem;
					endState = null;
				}
			}
		}
		
		if (canvasMode == CanvasMode.SELECTING) {
			if (e.button == 1) {
				boolean haveCPHit = false;
				boolean haveTransition = false;
				if (!selection.isEmpty()) {
					
					for (IDocumentItem crItem : selection.values()) {
						if (crItem instanceof ITransition) {
							haveTransition = true;
							ITransition crTrans = (ITransition) crItem;
							
							hitTestTransition(e.x, e.y, crTrans, hitCP);
							
							if (hitCP[0] || hitCP[1] || 
									hitCP[2] || hitCP[3]) {
								haveCPHit = true;
								break;
							}
								
						}
					}
				}
				
				if (!haveTransition || !haveCPHit) {
					if (hoverItem != null && !isSelected(hoverItem.getName())) {
						if (hoverItem instanceof IState) {
							selectState(hoverItem.getName(), true);
						} else {
							selectTransition(hoverItem.getName(), true);
						}
						
					} else {
						if (hoverItem == null)
							selection.clear();
					}
				}
				
				redraw();
			}
		}
	
		oldMouseX = e.x;
		oldMouseY = e.y;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseUp(MouseEvent e) {
		
		if (canvasMode == CanvasMode.ADD_TRANSITION) {
			if (e.button == 1) {
				if (hoverItem != null && hoverItem instanceof IState) {				
					endState = (IState)hoverItem;
					
					fireAddTransition(startState, endState);
					
				} 
				
				startState = null;
				endState = null;
				
				redraw();
			}
		}
		mousePressed = false;
		mouseButton = 0;
	}

	// MouseMove Interface
	
	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseMoveListener#mouseMove(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseMove(MouseEvent e) {
		float[] mouseCoord = new float[] {oldMouseX, oldMouseY,
				e.x, e.y};
		
		getInvDocumentTransform().transform(mouseCoord);
		
		if (canvasMode == CanvasMode.ADD_STATE) {
			redraw();
		}
		
		if (canvasMode == CanvasMode.PANNING) {

			if (mousePressed && mouseButton == 1) {
				
				
				Point viewPos = theDocument.getViewPosition();
				viewPos.coords[0] += 
					(mouseCoord[2] - mouseCoord[0])*theDocument.getZoom();
				viewPos.coords[1] += 
					(mouseCoord[3] - mouseCoord[1])*theDocument.getZoom();
				
				theDocument.setViewPosition(viewPos);
				
				redraw();
			}
		}
		
		if (canvasMode == CanvasMode.SELECTING || canvasMode == CanvasMode.ADD_TRANSITION) {
			
			if (!mousePressed || canvasMode == CanvasMode.ADD_TRANSITION) {
				// Doing hit tests
				IDocumentItem oldHover = hoverItem;
				
				hoverItem = null;
				
				for (ITransition crTrans : theDocument.getTransitions().values()) {
					if (hitTestTransition(e.x, e.y, crTrans, null)) {
						hoverItem = crTrans;
						break;
					}
				}				
				
				if (hoverItem == null) {
					for (IState crState : theDocument.getStates().values()) {
						if (hitTestState(e.x, e.y, crState)) {
							hoverItem = crState;
							break;
						}
					}
				}
				
				if (hoverItem != oldHover || canvasMode == CanvasMode.ADD_TRANSITION)
					redraw();
				
				
			} else {
				if (!selection.isEmpty() && canvasMode == CanvasMode.SELECTING) {
					for (IDocumentItem crItem : selection.values()) {
						if (crItem instanceof IState) {
							IState crState = (IState)crItem;
							
							Point pos = crState.getPosition();
							
							pos.coords[0] += 
								(mouseCoord[2] - mouseCoord[0]);
							pos.coords[1] += 
								(mouseCoord[3] - mouseCoord[1]);
							
							crState.setPosition(pos);
						}
						
						if (crItem instanceof ITransition) {
							ITransition crTrans = (ITransition) crItem;
							
							if (hitCP[1]) {
								Point pos = crTrans.getStartControlPoint();
								
								pos.coords[0] += 
									(mouseCoord[2] - mouseCoord[0]);
								pos.coords[1] += 
									(mouseCoord[3] - mouseCoord[1]);
								
								crTrans.setStartControlPoint(pos);
							} else if (hitCP[2]) {
								Point pos = crTrans.getEndControlPoint();
								
								pos.coords[0] += 
									(mouseCoord[2] - mouseCoord[0]);
								pos.coords[1] += 
									(mouseCoord[3] - mouseCoord[1]);
								
								crTrans.setEndControlPoint(pos);
							}
						}
					}
					
					redraw();
				}
			}
		}
		
		oldMouseX = e.x;
		oldMouseY = e.y;
		
		
	}
	
	// MouseTrack Interface

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseTrackListener#mouseEnter(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseEnter(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseTrackListener#mouseExit(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseExit(MouseEvent e) {
		
		if (hoverItem != null) {
			hoverItem = null;
			redraw();
		}
		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.MouseTrackListener#mouseHover(org.eclipse.swt.events.MouseEvent)
	 */
	public void mouseHover(MouseEvent e) {
		// TODO Auto-generated method stub
		
	}
	

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.KeyListener#keyPressed(org.eclipse.swt.events.KeyEvent)
	 */
	public void keyPressed(KeyEvent e) {
		if (canvasMode == CanvasMode.SELECTING) {
			if (e.keyCode == SWT.DEL) {
				if (!selection.isEmpty()) {
					for (IDocumentItem crItem : selection.values()) {
						fireItemDelete(crItem);
						
						selection.remove(crItem.getName());
					}
					
				}
			}
		}
		
		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.swt.events.KeyListener#keyReleased(org.eclipse.swt.events.KeyEvent)
	 */
	public void keyReleased(KeyEvent e) {
		// TODO Auto-generated method stub
		
	}

	/**
	 * 
	 * @return the document that is being drawn
	 */
	public IDocument getDocument() {
		return theDocument;
	}
	
	/**
	 * 
	 * @param document the StaDiUM document to set active
	 */
	public void setDocument(IDocument document) {
		if (this.theDocument != document) {
			this.theDocument = document;
			
			redraw();
		}
	}
	
	public CanvasMode getCanvasMode() {
		return canvasMode;
	}
	
	public void setCanvasMode(CanvasMode mode) {
		if (mode == null)
			throw new NullPointerException("mode is null");
		this.canvasMode = mode;
		
		if (mode == CanvasMode.PANNING) {
			this.setCursor(new Cursor(display, SWT.CURSOR_HAND));
		} else if (mode == CanvasMode.SELECTING) {
			this.setCursor(new Cursor(display, SWT.CURSOR_ARROW));
		} else if (mode == CanvasMode.ADD_TRANSITION){
			this.startState = null;
			this.endState = null;
		} else {
			// Do custom stuff here
		}
	}
	
	public boolean isDrawingGrid() {
		return drawGrid;
	}
	
	public void setDrawingGrid(boolean value) {
		this.drawGrid = value;
	}
	
	// SELECTION MANAGEMENT ROUTINES
	
	/**
	 * 
	 * @param item
	 */
	public void selectState(String name, boolean exclusive) {
		if (exclusive)
			selection.clear();
		
		if (name != null)
			selection.put(name, theDocument.getStates().get(name));
	}
	
	public void selectTransition(String name, boolean exclusive) {
		if (exclusive)
			selection.clear();
		
		if (name != null)
			selection.put(name, theDocument.getTransitions().get(name));
	}
	
	public boolean isSelected(String name) {
		if (name != null)
			return selection.containsKey(name);
		
		return false;
	}

	/**
	 * Disposes the SWT resources allocated by the object
	 */
	@Override
	public void dispose() {
		for (Color crColor : colors.values()) {
			crColor.dispose();
		}
		
		if (docTransform != null) {
			docTransform.dispose();
		}
		
		if (docInvTransform != null) {
			docInvTransform.dispose();
		}
		
		super.dispose();
	}
	
	// Event subscription management
	
	public void addDrawingEventListener(DrawingEventListener l) {
		drawingEventListenerList.add(DrawingEventListener.class, l);
	}
	
	public void removeDrawingEventListener(DrawingEventListener l) {
		drawingEventListenerList.remove(DrawingEventListener.class, l);
	}
	
	public DrawingEventListener[] getDrawingEventListeners() {
		return drawingEventListenerList.getListeners(DrawingEventListener.class);
	}
}
